﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace FastMVVM.Core
{
    /// <summary>
    /// This is responsible for instanciate any ViewModel.
    /// With this you can manage through instanciate a new, or just get a singleton instance.
    /// </summary>
    public static class ViewModelFactory
    {
        /// <summary>
        /// Singleton's Storage.
        /// </summary>
        private static Dictionary<Type, ViewModelBase> _singletons;

        /// <summary>
        /// StaticConstructor: Needed to instanciate private members.
        /// </summary>
        static ViewModelFactory()
        {
            _singletons = new Dictionary<Type, ViewModelBase>();
        }

        /// <summary>
        /// Give back a new instance of a ViewModel.
        /// </summary>
        /// <typeparam name="TViewModel">The expected ViewModel's type.</typeparam>
        /// <returns>A new instance of a ViewModel.</returns>
        public static TViewModel New<TViewModel>() where TViewModel : ViewModelBase
        {
            Type parent = typeof(TViewModel);
            AssemblyName name = new AssemblyName("Proxy");
            AssemblyBuilder assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
            ModuleBuilder module = assembly.DefineDynamicModule("Proxy");
            TypeBuilder mTypeBuilder = module.DefineType(parent.Name + "Proxy", TypeAttributes.Public);
            mTypeBuilder.SetParent(parent);

            MethodInfo raiseEvent = parent.GetMethod("NotifyPropertyChanged");

            foreach (PropertyInfo pinfo in parent.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (pinfo.GetSetMethod().IsVirtual)
                {
                    NotifyPropertyChanged[] attrs = pinfo.GetCustomAttributes(typeof(NotifyPropertyChanged), true) as NotifyPropertyChanged[];

                    PropertyBuilder pb = mTypeBuilder.DefineProperty(
                        pinfo.Name, PropertyAttributes.None, pinfo.PropertyType, Type.EmptyTypes);
                    MethodAttributes getSetAttr =
                        MethodAttributes.Public | MethodAttributes.SpecialName |
                        MethodAttributes.HideBySig | MethodAttributes.Virtual;
                    MethodBuilder getMethod =
                        mTypeBuilder.DefineMethod(
                            "get_" + pinfo.Name, getSetAttr, pinfo.PropertyType, Type.EmptyTypes);
                    ILGenerator gen = getMethod.GetILGenerator();
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Call, pinfo.GetGetMethod());
                    gen.Emit(OpCodes.Ret);
                    pb.SetGetMethod(getMethod);

                    MethodBuilder setMethod =
                        mTypeBuilder.DefineMethod(
                            "set_" + pinfo.Name, getSetAttr, null, new Type[] { pinfo.PropertyType });
                    gen = setMethod.GetILGenerator();
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Ldarg_1);
                    gen.Emit(OpCodes.Call, pinfo.GetSetMethod());
                    foreach (NotifyPropertyChanged n in attrs)
                    {
                        gen.Emit(OpCodes.Ldarg_0);
                        //gen.Emit(OpCodes.Ldstr, pinfo.Name);
                        if (String.IsNullOrEmpty(n.PropertyName))
                            gen.Emit(OpCodes.Ldstr, pinfo.Name);
                        else
                            gen.Emit(OpCodes.Ldstr, n.PropertyName);
                        gen.Emit(OpCodes.Call, raiseEvent);
                    }
                    gen.Emit(OpCodes.Ret);
                    pb.SetSetMethod(setMethod);
                }
            }

            Type proxyType = mTypeBuilder.CreateType();
            ConstructorInfo ctr = proxyType.GetConstructor(new Type[] { });
            Object proxy = ctr.Invoke(new Type[] { });

            return proxy as TViewModel;
        }

        /// <summary>
        /// At first call, give a new instance of a ViewModel.
        /// Any other calls will give the same instance.
        /// WARNING: If called after a "New" command, will not give that instance.
        /// </summary>
        /// <typeparam name="TViewModel">The expected ViewModel's type.</typeparam>
        /// <returns>At first call, give a new instance of a ViewModel. Any other calls give the same instance.</returns>
        public static TViewModel GetInstance<TViewModel>() where TViewModel : ViewModelBase
        {
            Type vmType = typeof(TViewModel);

            if (!_singletons.ContainsKey(vmType))
                _singletons.Add(vmType, New<TViewModel>());

            return _singletons[vmType] as TViewModel;
        }
    }
}
